Skip to content

Compose tab for MUSH poses, and post-crash diagnostics log viewer - #27

Merged
HarryCordewener merged 19 commits into
masterfrom
feature/compose-tab-and-diagnostics
Aug 11, 2026
Merged

Compose tab for MUSH poses, and post-crash diagnostics log viewer#27
HarryCordewener merged 19 commits into
masterfrom
feature/compose-tab-and-diagnostics

Conversation

@HarryCordewener

@HarryCordewener HarryCordewener commented Aug 11, 2026

Copy link
Copy Markdown
Member

Adds two features: a dedicated screen for composing MUSH poses, and a way to read the on-device diagnostics log after the client has restarted.

Design: docs/superpowers/specs/2026-08-10-compose-and-diagnostics-design.md
Plan: docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md

Compose tab

A pose is multi-line prose, but the wire protocol wants one line. Doing that conversion by hand is error-prone, so poses get written elsewhere and pasted in mangled.

/compose is a fifth nav tab whose editor fills the tab body: a prefix chip row on top (say, pose, semipose, @emit, plus a free-text custom prefix remembered per world), a flex: 1 middle region holding either the editor or a read-only preview of the exact wire text at the same size, and a footer with a character count, Clear, Preview/Edit, and Send. Ctrl/Cmd+Enter sends; a bare Enter inserts a newline.

MushPoseFormatter is a pure static class holding the whole escaping contract: normalise line endings, escape %%%, trim trailing whitespace and drop leading/trailing blank lines, then replace newlines with %r — in that order, so the inserted markers are not themselves escaped. A prefix ending in =, /, or whitespace joins verbatim (page Bob=page Bob=He grins…); anything else gets one space.

Drafts are held per session, so switching tabs mid-pose doesn't clobber one, and text typed before any session exists is carried into the first session that connects.

Accepted trade-off: literal % escaping means a deliberately typed %r or ansi(...) is neutered to %%r. The preview shows the exact wire text, so it is visible before sending rather than surprising afterwards. There is no raw mode.

Post-crash diagnostics

The write side already existed — a rotated log under the app's private data dir, fed by FileLoggerProvider and by the AppDomain / TaskScheduler / AndroidEnvironment crash hooks. The only way to read it was the share sheet, so a crash could not be investigated on the device where it happened.

  • FileLogStore and FileLoggerProvider move from SharpClient.App into SharpClient.Core, taking the log directory as a constructor argument. Both previously lived in the MAUI head, which no test project can reference — this is what puts the write format and the rotation logic under test for the first time.
  • LogEntryParser reverses the writer's line format, keeping multi-line stack traces attached to their entry, discarding a rotation-severed leading fragment, and skipping a corrupt-but-header-shaped timestamp rather than throwing.
  • ILogReader / FileLogReader merge the rotated backup and the current file newest-first. WriteException also drops a last-crash.txt sidecar, so the launch-time check is one small file read and survives rotation.
  • /diagnostics lists entries with All / Errors / Crashes filters and Refresh / Copy / Share / Clear. Clear is a two-step button rather than a JS confirm(), which would block the Blazor circuit inside the Android WebView.
  • CrashBanner renders in MainLayout when a crash marker is pending and deep-links to /diagnostics?filter=crashes.

The Web host has no persistent log and takes NoopLogReader, so neither the viewer nor the banner appears there.

Out of scope: OOM kills and native crashes never reach the managed hooks, so a run ending that way leaves no marker and shows no banner. Detecting that would need a clean-shutdown sentinel.

Incidental fix

3089f49 pins AngleSharp 1.5.2 in the bUnit test project. A NuGet advisory (GHSA-pgww-w46g-26qg) against the 1.4.0 that bunit 2.7.2 resolves transitively began failing the audit, and TreatWarningsAsErrors turns NU1902 into a build error — so SharpClient.UI.Tests had stopped building on every branch, master included. 1.5.0 is the first patched release. Worth dropping the pin when bunit is next bumped to 2.8.6+, which brings a patched AngleSharp transitively.

Testing

262 core / 75 UI / 17 data, all green in Release. Web host and Android head both build Release with 0 warnings.

Coverage added: the formatter's escaping order, line-ending forms, and every separator branch; ComposeViewModel send-gating, per-session drafts, per-world prefix persistence; FileLogStore write and rotation against a temp directory; parser edge cases including multi-line exceptions and corrupt input; reader ordering across the rotation boundary and the crash-marker lifecycle; and bUnit coverage for the compose chord, the diagnostics filters, Share/Copy failure paths, and the banner.

Every task was reviewed independently and the whole branch got a final review pass; the fixes from both are in the history.

Still needs a device run. The MAUI-only paths — the real log file, the share sheet, and the banner after an actual crash — are covered by tests and the Android build but have not been exercised on hardware.

Known follow-ups (non-blocking)

  • DiagnosticsView.Visible re-filters and re-allocates on every access.
  • FileLogReader.ReadAsync is synchronous behind an async signature; the read plus parse happens on the UI thread.
  • _truncated is a false positive when the log is exactly MaxEntries long.
  • ReadAsync throws on a negative maxEntries (unreachable today).
  • LogEntryParser drops blank lines inside a detail block.
  • ComposeViewModel.CustomPrefix persists on every keystroke.

🤖 Generated with Claude Code

https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852

Summary by CodeRabbit

  • New Features
    • Added a Compose experience for formatting, previewing, and sending MUSH poses.
    • Added per-session drafts and per-world custom prefix settings.
    • Added diagnostics viewing with filtering, copying, sharing, clearing, and crash details.
    • Added a launch-time banner for crashes from a previous session.
    • Added log rotation and newest-first diagnostic history.
  • Bug Fixes
    • Improved handling of multiline text, formatting, malformed log entries, and unavailable diagnostics storage.
  • Tests
    • Added comprehensive coverage for Compose, formatting, diagnostics, crash reporting, and Settings integration.

HarryCordewener and others added 19 commits August 10, 2026 22:46
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
…entDefault flag

Ctrl/Cmd+Enter never suppressed the textarea's native newline insertion, so the
default DOM mutation and its paired oninput could race the async SendAsync() and
leave a stray newline in the draft after sending. @onkeydown:preventDefault can
only react to the *previous* render, so arm it from the modifier's own keydown,
one event ahead, while the actual send decision keeps using the Enter event's
own accurate CtrlKey/MetaKey. Add coverage for both halves of the chord.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
…rd test coverage

The guard suppressed the native default for whatever key followed a Ctrl/Cmd
keydown, breaking Ctrl+C/V/A/Z inside the textarea for that one keystroke — a
worse regression than the race it guarded against. The race itself doesn't
survive scrutiny: MushPoseFormatter drops trailing blank lines, so a stray
newline can never reach the wire, and either event ordering converges on an
empty textarea since Blazor diffs value="@Vm.Body" against its own
last-rendered value, not the live DOM. OnKeyDown reverts to the plain
Ctrl/Meta+Enter check; the two keyboard tests from the previous round stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
…Core

Also bumps SharpClient.UI's Microsoft.Extensions.DependencyInjection.Abstractions
pin to 10.0.9 to match the version Core now pulls in transitively via
Microsoft.Extensions.Logging.Abstractions, resolving an NU1605 downgrade error
in the Android head build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
…Unit tests

bunit 2.7.2 resolves AngleSharp 1.4.0 transitively. A NuGet advisory for that
version now fails the audit, and TreatWarningsAsErrors turns NU1902 into a
build error, so the UI test project stopped building on every branch including
master. Pinning the patched version restores the build without weakening the
audit. Matches the pin SharpMUSH applied for the same advisory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
… gating

- Compose: keep a fallback draft while no session is active and adopt it
  into the first session that becomes active, instead of discarding text
  typed before connecting.
- Diagnostics: re-apply InitialFilter on OnParametersSet (not just on init)
  so the crash banner's deep link works while already on /diagnostics,
  without clobbering a manually chosen filter chip.
- Settings: gate the "View log" row on ILogReader.IsAvailable and "Export
  log" on ILogExporter.IsAvailable, matching the design doc, instead of
  gating both off the exporter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
- Clear() now empties the pending draft when no session is active, so the
  button isn't a dead no-op now that Body persists text typed before a
  session connects.
- Add SettingsView coverage proving the View log / Export log rows are
  gated independently (reader-only and exporter-only host combinations).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MPRzndB5egy4F2A3q5852
Copilot AI lite review requested due to automatic review settings August 11, 2026 18:10
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a Compose workflow with MUSH formatting, session drafts, UI controls, and navigation. Moves diagnostics into Core with file logging, crash markers, readers, viewing, filtering, host registration, and tests.

Changes

Compose and diagnostics

Layer / File(s) Summary
Feature contracts and implementation plan
docs/superpowers/...
Defines Compose and diagnostics APIs, architecture, UI behavior, host boundaries, and validation requirements.
Compose formatting, state, and UI
src/SharpClient.Core/Formatting/..., src/SharpClient.Core/Presentation/..., src/SharpClient.UI/..., tests/SharpClient.Tests/Formatting/..., tests/SharpClient.Tests/Presentation/..., tests/SharpClient.UI.Tests/ComposeViewTests.cs
Adds MUSH pose formatting, per-session drafts, persisted prefixes, send and clear behavior, Compose navigation, preview controls, keyboard handling, styling, and tests.
Diagnostic storage, parsing, and readers
src/SharpClient.Core/Diagnostics/..., src/SharpClient.App/..., src/SharpClient.Web/Program.cs, tests/SharpClient.Tests/Diagnostics/...
Moves file logging into Core, adds rotation and crash markers, parses log entries, provides file and no-op readers, wires hosts, and tests storage behavior.
Diagnostics viewing and crash notification
src/SharpClient.UI/Components/..., src/SharpClient.UI/Pages/..., src/SharpClient.UI/Layout/..., src/SharpClient.UI/wwwroot/..., tests/SharpClient.UI.Tests/...
Adds diagnostics filtering and actions, Settings links, clipboard support, crash-banner handling, routes, styling, and UI tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

Compose send flow

sequenceDiagram
  participant ComposeView
  participant ComposeViewModel
  participant MushPoseFormatter
  participant ISession
  ComposeView->>ComposeViewModel: Submit body and prefix
  ComposeViewModel->>MushPoseFormatter: Format command
  MushPoseFormatter-->>ComposeViewModel: Return formatted command
  ComposeViewModel->>ISession: Send formatted line
  ISession-->>ComposeViewModel: Complete send
  ComposeViewModel-->>ComposeView: Clear draft and refresh
Loading

Crash diagnostics flow

sequenceDiagram
  participant CrashBanner
  participant ILogReader
  participant DiagnosticsPage
  participant DiagnosticsView
  CrashBanner->>ILogReader: Load pending crash
  ILogReader-->>CrashBanner: Return CrashReport
  CrashBanner->>DiagnosticsPage: Navigate with crash filter
  DiagnosticsPage->>DiagnosticsView: Pass filter parameter
  DiagnosticsView->>ILogReader: Read diagnostic entries
  ILogReader-->>DiagnosticsView: Return log entries
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely identifies both primary changes: the MUSH pose Compose tab and the post-crash diagnostics log viewer.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds two end-user features to SharpClient’s UI and Core layers: (1) a dedicated Compose tab for writing multi-line MUSH poses with deterministic wire-format escaping, and (2) an in-app diagnostics log viewer with a “crashed last run” banner backed by a Core-level file log reader/parser.

Changes:

  • Introduces /compose (nav tab) with MushPoseFormatter + ComposeViewModel and a full-height editor/preview UI.
  • Moves diagnostics logging primitives into SharpClient.Core, adds parsing + reading + crash-marker support, and exposes them via /diagnostics plus a CrashBanner.
  • Expands test coverage (TUnit + bUnit) for compose + diagnostics flows and pins AngleSharp in UI tests to satisfy NuGet audit.

Reviewed changes

Copilot reviewed 36 out of 36 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/SharpClient.UI.Tests/UiFakeLogReader.cs Adds a UI-test ILogReader fake for diagnostics and banner tests.
tests/SharpClient.UI.Tests/SharpClient.UI.Tests.csproj Pins AngleSharp to a patched version for audit compliance.
tests/SharpClient.UI.Tests/SettingsViewTests.cs Updates Settings tests for new diagnostics reader/exporter gating and UI rows.
tests/SharpClient.UI.Tests/DiagnosticsViewTests.cs Adds bUnit coverage for diagnostics filtering/actions/empty state.
tests/SharpClient.UI.Tests/CrashBannerTests.cs Adds bUnit coverage for crash banner render + dismiss behavior.
tests/SharpClient.UI.Tests/ComposeViewTests.cs Adds bUnit coverage for compose UI interactions and send behavior.
tests/SharpClient.Tests/Presentation/ComposeViewModelTests.cs Adds TUnit coverage for compose VM send gating, drafts, persistence.
tests/SharpClient.Tests/Formatting/MushPoseFormatterTests.cs Adds TUnit coverage for MUSH escaping/joining contract.
tests/SharpClient.Tests/Diagnostics/LogEntryParserTests.cs Adds TUnit coverage for parsing the log format and edge cases.
tests/SharpClient.Tests/Diagnostics/FileLogStoreTests.cs Adds TUnit coverage for file log writing + rotation behavior.
tests/SharpClient.Tests/Diagnostics/FileLogReaderTests.cs Adds TUnit coverage for reader ordering, truncation, crash marker lifecycle.
src/SharpClient.Web/Program.cs Registers NoopLogReader for the web host.
src/SharpClient.UI/wwwroot/sc-interop.js Adds clipboard helper copyText for diagnostics Copy action.
src/SharpClient.UI/wwwroot/app.css Adds styling for compose page, diagnostics page, and crash banner.
src/SharpClient.UI/SharpClient.UI.csproj Updates DI abstractions package version.
src/SharpClient.UI/ServiceCollectionExtensions.cs Registers ComposeViewModel for both hosts.
src/SharpClient.UI/Pages/DiagnosticsPage.razor Adds /diagnostics route with query-param filter wiring.
src/SharpClient.UI/Pages/ComposePage.razor Adds /compose route and injects the compose VM.
src/SharpClient.UI/Layout/MainLayout.razor Adds CrashBanner and adds Compose nav entry.
src/SharpClient.UI/Components/SettingsView.razor Adds “View log” row and shows Diagnostics section if reader/exporter available.
src/SharpClient.UI/Components/DiagnosticsView.razor Implements diagnostics viewer UI, filters, and actions.
src/SharpClient.UI/Components/CrashBanner.razor Implements “crashed last run” banner with deep-link + dismiss.
src/SharpClient.UI/Components/ComposeView.razor Implements compose editor/preview UI with prefix chips and send chord.
src/SharpClient.Core/SharpClient.Core.csproj Adds logging abstractions dependency needed by Core diagnostics logger provider.
src/SharpClient.Core/Presentation/ComposeViewModel.cs Implements per-session compose drafts + prefix persistence + send logic.
src/SharpClient.Core/Formatting/MushPoseFormatter.cs Implements the wire-format escaping and prefix/body joining rules.
src/SharpClient.Core/Diagnostics/LogEntryParser.cs Parses the file log format into LogEntry objects with multi-line detail.
src/SharpClient.Core/Diagnostics/LogEntry.cs Introduces LogEntry and CrashReport record types.
src/SharpClient.Core/Diagnostics/ILogReader.cs Adds ILogReader abstraction + NoopLogReader.
src/SharpClient.Core/Diagnostics/FileLogStore.cs Makes the log store platform-agnostic and adds crash marker sidecar.
src/SharpClient.Core/Diagnostics/FileLogReader.cs Implements merged rotated-log reading and crash marker reading/clearing.
src/SharpClient.Core/Diagnostics/FileLoggerProvider.cs Moves logger provider into Core diagnostics namespace.
src/SharpClient.App/Platforms/Android/MainActivity.cs Updates namespace imports for moved diagnostics types.
src/SharpClient.App/MauiProgram.cs Supplies log directory to Core log store and registers ILogReader.
docs/superpowers/specs/2026-08-10-compose-and-diagnostics-design.md Adds design spec for compose + diagnostics features.
docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md Adds detailed implementation plan and constraints.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +133 to +135
var text = string.Join('\n', Visible.Select(e =>
$"{e.Timestamp:yyyy-MM-dd HH:mm:ss} [{e.Level}] {e.Category}: {e.Message}"
+ (e.Detail is null ? string.Empty : "\n" + e.Detail)));
Comment on lines +11 to +22
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500)
{
var entries = new List<LogEntry>();
entries.AddRange(ReadFile(_store.BackupPath));
entries.AddRange(ReadFile(_store.FilePath));

var start = Math.Max(0, entries.Count - maxEntries);
var newest = entries.GetRange(start, entries.Count - start);
newest.Reverse();

return Task.FromResult<IReadOnlyList<LogEntry>>(newest);
}
@HarryCordewener
HarryCordewener merged commit dbaf5a9 into master Aug 11, 2026
3 of 4 checks passed
@HarryCordewener
HarryCordewener deleted the feature/compose-tab-and-diagnostics branch August 11, 2026 18:23

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md`:
- Around line 2354-2358: Update CopyAsync so each copied entry omits the “: ”
separator when e.Category is empty, while retaining the existing “{Category}:
{Message}” format for categorized entries and preserving the timestamp, level,
and detail formatting.
- Around line 1835-1841: Update WriteException to always record the exception
but only call WriteCrashMarker for process-level unhandled-exception sources;
ensure the TaskScheduler.UnobservedTaskException path in MauiProgram remains
log-only after e.SetObserved(), preventing recoverable task faults from creating
last-crash.txt or a crash banner.
- Around line 1847-1857: Update the write flow around Write, WriteException, and
WriteCrashMarker so the log write and crash-marker write execute under the same
_gate lock. Keep the lock held through the complete combined operation, rather
than releasing it before WriteCrashMarker, while preserving the existing
failure-swallowing behavior for marker writes.
- Around line 2640-2647: Update the MAUI crash validation steps after the Web
host checks to trigger a process-level failure reaching
AppDomain.UnhandledException or AndroidEnvironment.UnhandledExceptionRaiser,
rather than throwing from a Blazor component and hard-killing the app.
Alternatively, direct the test through an existing seam that writes the crash
marker, then relaunch the app and verify CrashBanner and the Crashes-filter
entry.
- Around line 2402-2406: Update DiagnosticsPage to check ILogReader.IsAvailable
before rendering DiagnosticsView, hiding the diagnostics route or redirecting
when the Web host uses NoopLogReader. Preserve the existing filter behavior for
available readers, and add a direct-navigation test verifying the Web host does
not expose the diagnostics viewer or its actions.
- Around line 1921-1931: Update ReadAsync to handle maxEntries values less than
or equal to zero before calculating the range, returning an empty IReadOnlyList
for those inputs; preserve the existing newest-entry ordering and limiting
behavior for positive values.
- Around line 630-642: Update ComposeView’s Body getter and setter to preserve
text entered while Active is null by reusing the existing _pendingDraft pattern
from ComposeViewModel. Return the pending value when no session is active, store
edits there instead of discarding them, and transfer it into the active draft
when a session becomes active. Add a regression test covering entry before
activation and preservation after activation.
- Around line 1606-1607: Update the timestamp parsing in FileLogReader.ReadFile
to use TryParseExact instead of ParseExact, and skip the current header line
when the timestamp is invalid. Preserve parsing of valid entries so one
malformed timestamp does not discard the entire file.
- Around line 1014-1020: Update ComposeView’s OnKeyDown handler to conditionally
prevent the browser default action when Enter is pressed with CtrlKey or
MetaKey, while leaving bare Enter unchanged for multiline input. Apply the
equivalent client-side prevention in both the interactive Web host and MAUI
Blazor WebView host implementations, alongside the existing SendAsync call.

In `@src/SharpClient.Core/Diagnostics/FileLogReader.cs`:
- Around line 11-21: Update the public ReadAsync method to validate maxEntries
before calculating start or calling GetRange, rejecting negative values with the
established argument-validation behavior; preserve the existing ordering and
limiting behavior for zero and positive values.

In `@src/SharpClient.Core/Diagnostics/FileLogStore.cs`:
- Around line 44-50: Update FileLogStore.WriteException so it records the
exception without creating last-crash.txt by default, and add a separate
termination-aware path or parameter that creates the marker only for handlers
indicating process termination. Change the TaskScheduler.UnobservedTaskException
handling in MauiProgram to use the non-terminating append path before
SetObserved, while preserving crash-marker creation for terminating handlers.
Add a regression test covering this lifecycle and verifying no false crash
banner marker is left.

In `@src/SharpClient.UI/Components/ComposeView.razor`:
- Around line 85-91: Update the ComposeView key handling around OnKeyDown so
Ctrl/Cmd+Enter is processed by a DOM listener that calls preventDefault() only
for that shortcut, preventing the textarea from inserting a newline while
preserving bare Enter behavior. Retain SendAsync() for the shortcut and add a
browser-level test verifying the editor remains empty after sending.

In `@src/SharpClient.UI/Pages/DiagnosticsPage.razor`:
- Around line 1-5: Update the DiagnosticsPage component to inject ILogReader as
Reader and conditionally render DiagnosticsView only when Reader.IsAvailable is
true, while preserving the existing InitialFilter binding.

In `@tests/SharpClient.UI.Tests/UiFakeLogReader.cs`:
- Around line 14-15: Update UiFakeLogReader.ReadAsync to return a snapshot of
Entries limited to maxEntries rather than the entire collection. Preserve the
IReadOnlyList<LogEntry> result and ensure the original Entries collection is not
modified.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4ccd1bf9-b6d9-4b00-956b-a358aef5a27c

📥 Commits

Reviewing files that changed from the base of the PR and between a32b684 and 3a0ce20.

📒 Files selected for processing (36)
  • docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md
  • docs/superpowers/specs/2026-08-10-compose-and-diagnostics-design.md
  • src/SharpClient.App/MauiProgram.cs
  • src/SharpClient.App/Platforms/Android/MainActivity.cs
  • src/SharpClient.Core/Diagnostics/FileLogReader.cs
  • src/SharpClient.Core/Diagnostics/FileLogStore.cs
  • src/SharpClient.Core/Diagnostics/FileLoggerProvider.cs
  • src/SharpClient.Core/Diagnostics/ILogReader.cs
  • src/SharpClient.Core/Diagnostics/LogEntry.cs
  • src/SharpClient.Core/Diagnostics/LogEntryParser.cs
  • src/SharpClient.Core/Formatting/MushPoseFormatter.cs
  • src/SharpClient.Core/Presentation/ComposeViewModel.cs
  • src/SharpClient.Core/SharpClient.Core.csproj
  • src/SharpClient.UI/Components/ComposeView.razor
  • src/SharpClient.UI/Components/CrashBanner.razor
  • src/SharpClient.UI/Components/DiagnosticsView.razor
  • src/SharpClient.UI/Components/SettingsView.razor
  • src/SharpClient.UI/Layout/MainLayout.razor
  • src/SharpClient.UI/Pages/ComposePage.razor
  • src/SharpClient.UI/Pages/DiagnosticsPage.razor
  • src/SharpClient.UI/ServiceCollectionExtensions.cs
  • src/SharpClient.UI/SharpClient.UI.csproj
  • src/SharpClient.UI/wwwroot/app.css
  • src/SharpClient.UI/wwwroot/sc-interop.js
  • src/SharpClient.Web/Program.cs
  • tests/SharpClient.Tests/Diagnostics/FileLogReaderTests.cs
  • tests/SharpClient.Tests/Diagnostics/FileLogStoreTests.cs
  • tests/SharpClient.Tests/Diagnostics/LogEntryParserTests.cs
  • tests/SharpClient.Tests/Formatting/MushPoseFormatterTests.cs
  • tests/SharpClient.Tests/Presentation/ComposeViewModelTests.cs
  • tests/SharpClient.UI.Tests/ComposeViewTests.cs
  • tests/SharpClient.UI.Tests/CrashBannerTests.cs
  • tests/SharpClient.UI.Tests/DiagnosticsViewTests.cs
  • tests/SharpClient.UI.Tests/SettingsViewTests.cs
  • tests/SharpClient.UI.Tests/SharpClient.UI.Tests.csproj
  • tests/SharpClient.UI.Tests/UiFakeLogReader.cs

Comment on lines +630 to +642
public string Body
{
get => Active is not null && _drafts.TryGetValue(Active, out var draft) ? draft : string.Empty;
set
{
if (Active is null)
{
return;
}

_drafts[Active] = value;
Changed?.Invoke();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve drafts entered before session activation.

ComposeView remains editable when Active is null, but this setter discards every value and the getter returns an empty string. Users can type a draft, connect a session, and lose the draft.

Store a pending draft, or disable the editor when no session is active. Add a regression test. The downstream src/SharpClient.Core/Presentation/ComposeViewModel.cs implementation already uses _pendingDraft.

Proposed fix
+    private string _pendingDraft = string.Empty;
+
     public string Body
     {
-        get => Active is not null && _drafts.TryGetValue(Active, out var draft) ? draft : string.Empty;
+        get => Active is null
+            ? _pendingDraft
+            : _drafts.TryGetValue(Active, out var draft) ? draft : string.Empty;
         set
         {
             if (Active is null)
             {
+                _pendingDraft = value;
+                Changed?.Invoke();
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md` around lines
630 - 642, Update ComposeView’s Body getter and setter to preserve text entered
while Active is null by reusing the existing _pendingDraft pattern from
ComposeViewModel. Return the pending value when no session is active, store
edits there instead of discarding them, and transfer it into the active draft
when a session becomes active. Add a regression test covering entry before
activation and preservation after activation.

Comment on lines +1014 to +1020
// Ctrl/Cmd+Enter sends; a bare Enter has to stay a newline in a multi-line composer.
private async Task OnKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter" && (e.CtrlKey || e.MetaKey))
{
await SendAsync();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md"

printf '%s\n' '--- target section ---'
sed -n '980,1045p' "$file"

printf '%s\n' '--- relevant symbols and prevention APIs ---'
rg -n -C 3 'OnKeyDown|preventDefault|KeyboardEventArgs|SendAsync|ComposeView|textarea' "$file"

Repository: SharpMUSH/SharpClient

Length of output: 15186


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- project targets and existing event modifiers ---'
rg -n -C 3 'TargetFramework|Microsoft\.AspNetCore\.Components|`@onkeydown`|preventDefault|KeyboardEventArgs' \
  --glob '*.csproj' --glob '*.razor' --glob '*.cs' --glob '*.md' .

printf '%s\n' '--- compose test plan around keyboard behavior ---'
sed -n '768,935p' docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md

Repository: SharpMUSH/SharpClient

Length of output: 39586


🌐 Web query:

ASP.NET Core Blazor event handling preventDefault keyboard event textarea @onkeydown:preventDefault official documentation

💡 Result:

In ASP.NET Core Blazor, you can prevent the default browser behavior for keyboard events—such as typing characters into a textarea—by using the @on{DOM EVENT}:preventDefault directive attribute [1][2]. To prevent a keydown event's default action (e.g., stopping a character from appearing in a textarea), add @onkeydown:preventDefault to the element [1][3]. Usage Examples: 1. Static Prevention: To unconditionally prevent the default behavior, include the attribute without a value (which is equivalent to setting it to true) [1][2]: <textarea @onkeydown:preventDefault></textarea> 2. Conditional Prevention: You can also bind the attribute to a boolean expression to dynamically decide whether to prevent the default action [1][2]: <textarea @onkeydown:preventDefault="shouldPreventDefault" /> @code { private bool shouldPreventDefault = true; } Key Considerations: - Synchronous Execution: Because Blazor event handling (especially in Blazor Server) is asynchronous, you cannot call preventDefault inside a C# event handler method after the event has already been processed [4][5]. The @on{DOM EVENT}:preventDefault directive works because it allows Blazor to instruct the browser to prevent the default action before the event is sent to your C# code [6]. - Combining Attributes: You can use @onkeydown:preventDefault alongside a standard event handler [1][3]: <textarea @onkeydown="HandleKeyDown" @onkeydown:preventDefault />

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- implemented ComposeView and keyboard tests ---'
sed -n '1,125p' src/SharpClient.UI/Components/ComposeView.razor
sed -n '1,120p' tests/SharpClient.UI.Tests/ComposeViewTests.cs

printf '%s\n' '--- host render modes and WebView setup ---'
rg -n -C 4 'AddInteractive|RenderMode|BlazorWebView|RootComponents|SharpClient.UI|MapRazorComponents|UseStaticFiles' \
  src --glob '*.cs' --glob '*.razor' --glob '*.csproj'

Repository: SharpMUSH/SharpClient

Length of output: 21113


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

component = Path("src/SharpClient.UI/Components/ComposeView.razor").read_text()
web_program = Path("src/SharpClient.Web/Program.cs").read_text()
maui_program = Path("src/SharpClient.App/MauiProgram.cs").read_text()

textarea = re.search(r"<textarea\b(?P<body>.*?)</textarea>", component, re.S)
handler = re.search(r"private async Task OnKeyDown\(KeyboardEventArgs e\)(?P<body>.*?)(?=\n    private async Task SendAsync)", component, re.S)

assert textarea and handler
print("textarea_has_prevent_default:", ":preventDefault" in textarea.group("body"))
print("handler_checks_modifier_enter:",
      'e.Key == "Enter" && (e.CtrlKey || e.MetaKey)' in handler.group("body"))
print("handler_calls_send:", "await SendAsync();" in handler.group("body"))
print("web_uses_interactive_server:", "AddInteractiveServerComponents()" in web_program)
print("maui_uses_blazor_webview:", "AddMauiBlazorWebView()" in maui_program)
PY

printf '%s\n' '--- shared JS interop surface ---'
sed -n '1,220p' src/SharpClient.UI/wwwroot/sc-interop.js

Repository: SharpMUSH/SharpClient

Length of output: 9783


Prevent the default action for modifier+Enter.

ComposeView can insert a newline while SendAsync clears Vm.Body. Add client-side conditional prevention for Enter with CtrlKey or MetaKey. Keep bare Enter available for multiline input. Cover both the interactive Web host and the MAUI Blazor WebView host.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md` around lines
1014 - 1020, Update ComposeView’s OnKeyDown handler to conditionally prevent the
browser default action when Enter is pressed with CtrlKey or MetaKey, while
leaving bare Enter unchanged for multiline input. Apply the equivalent
client-side prevention in both the interactive Web host and MAUI Blazor WebView
host implementations, alongside the existing SendAsync call.

Comment on lines +1606 to +1607
timestamp = DateTimeOffset.ParseExact(
header.Groups["ts"].Value, TimestampFormat, CultureInfo.InvariantCulture);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip malformed timestamps instead of failing the complete parse.

ParseExact throws when a header-shaped line contains an invalid date. FileLogReader.ReadFile then catches the exception and returns no entries, so one corrupt line hides all valid entries in that file.

Use TryParseExact and skip invalid headers. This matches the malformed-input requirement and the current parser implementation.

Proposed fix
-                timestamp = DateTimeOffset.ParseExact(
-                    header.Groups["ts"].Value, TimestampFormat, CultureInfo.InvariantCulture);
+                if (!DateTimeOffset.TryParseExact(
+                    header.Groups["ts"].Value,
+                    TimestampFormat,
+                    CultureInfo.InvariantCulture,
+                    DateTimeStyles.None,
+                    out timestamp))
+                {
+                    continue;
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
timestamp = DateTimeOffset.ParseExact(
header.Groups["ts"].Value, TimestampFormat, CultureInfo.InvariantCulture);
if (!DateTimeOffset.TryParseExact(
header.Groups["ts"].Value,
TimestampFormat,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out timestamp))
{
continue;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md` around lines
1606 - 1607, Update the timestamp parsing in FileLogReader.ReadFile to use
TryParseExact instead of ParseExact, and skip the current header line when the
timestamp is invalid. Preserve parsing of valid entries so one malformed
timestamp does not discard the entire file.

Comment on lines +1835 to +1841
/// <summary>Records an unhandled exception captured by one of the global hooks.</summary>
public void WriteException(string source, Exception? ex)
{
var block = FormatEntry("CRASH", source, ex?.Message ?? "(no exception object)", ex);
Write(block);
WriteCrashMarker(block);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Separate recoverable task faults from crash markers.

WriteException always writes [CRASH] and last-crash.txt. src/SharpClient.App/MauiProgram.cs calls it from TaskScheduler.UnobservedTaskException and then calls e.SetObserved(). That path does not necessarily terminate the app.

A later launch can therefore show a false crash banner. Keep the exception in the log, but write the crash marker only for process-level unhandled-exception paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md` around lines
1835 - 1841, Update WriteException to always record the exception but only call
WriteCrashMarker for process-level unhandled-exception sources; ensure the
TaskScheduler.UnobservedTaskException path in MauiProgram remains log-only after
e.SetObserved(), preventing recoverable task faults from creating last-crash.txt
or a crash banner.

Comment on lines +1847 to +1857
private void WriteCrashMarker(string block)
{
try
{
File.WriteAllText(CrashMarkerPath, block);
}
catch
{
// Same contract as the log write: recording a crash must not cause another one.
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Serialize crash-marker writes with log writes.

WriteCrashMarker runs after Write releases _gate. Concurrent WriteException calls can overwrite each other or fail one marker write. The catch block hides that loss.

Protect the complete log-and-marker operation with the same lock.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-08-10-compose-and-diagnostics.md` around lines
1847 - 1857, Update the write flow around Write, WriteException, and
WriteCrashMarker so the log write and crash-marker write execute under the same
_gate lock. Keep the lock held through the complete combined operation, rather
than releasing it before WriteCrashMarker, while preserving the existing
failure-swallowing behavior for marker writes.

Comment on lines +11 to +21
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500)
{
var entries = new List<LogEntry>();
entries.AddRange(ReadFile(_store.BackupPath));
entries.AddRange(ReadFile(_store.FilePath));

var start = Math.Max(0, entries.Count - maxEntries);
var newest = entries.GetRange(start, entries.Count - start);
newest.Reverse();

return Task.FromResult<IReadOnlyList<LogEntry>>(newest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate maxEntries before calculating the range.

If maxEntries is negative, line 18 can pass an invalid count to List.GetRange and throw. Reject negative values at the public API boundary.

Proposed fix
 public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500)
 {
+    ArgumentOutOfRangeException.ThrowIfNegative(maxEntries);
+
     var entries = new List<LogEntry>();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500)
{
var entries = new List<LogEntry>();
entries.AddRange(ReadFile(_store.BackupPath));
entries.AddRange(ReadFile(_store.FilePath));
var start = Math.Max(0, entries.Count - maxEntries);
var newest = entries.GetRange(start, entries.Count - start);
newest.Reverse();
return Task.FromResult<IReadOnlyList<LogEntry>>(newest);
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500)
{
ArgumentOutOfRangeException.ThrowIfNegative(maxEntries);
var entries = new List<LogEntry>();
entries.AddRange(ReadFile(_store.BackupPath));
entries.AddRange(ReadFile(_store.FilePath));
var start = Math.Max(0, entries.Count - maxEntries);
var newest = entries.GetRange(start, entries.Count - start);
newest.Reverse();
return Task.FromResult<IReadOnlyList<LogEntry>>(newest);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClient.Core/Diagnostics/FileLogReader.cs` around lines 11 - 21,
Update the public ReadAsync method to validate maxEntries before calculating
start or calling GetRange, rejecting negative values with the established
argument-validation behavior; preserve the existing ordering and limiting
behavior for zero and positive values.

Comment on lines +44 to +50
/// <summary>Records an unhandled exception captured by one of the global hooks.</summary>
public void WriteException(string source, Exception? ex)
{
var block = FormatEntry("CRASH", source, ex?.Message ?? "(no exception object)", ex);
Write(block);
WriteCrashMarker(block);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not create a crash marker for every unhandled task exception.

WriteException always creates last-crash.txt. src/SharpClient.App/MauiProgram.cs invokes this method from TaskScheduler.UnobservedTaskException and then calls SetObserved at lines 62-65. The app can continue and exit cleanly, but the next launch will show a false crash banner.

Use Append for non-terminating task exceptions. Create the crash marker only from handlers that indicate process termination. Add a regression test for this lifecycle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClient.Core/Diagnostics/FileLogStore.cs` around lines 44 - 50,
Update FileLogStore.WriteException so it records the exception without creating
last-crash.txt by default, and add a separate termination-aware path or
parameter that creates the marker only for handlers indicating process
termination. Change the TaskScheduler.UnobservedTaskException handling in
MauiProgram to use the non-terminating append path before SetObserved, while
preserving crash-marker creation for terminating handlers. Add a regression test
covering this lifecycle and verifying no false crash banner marker is left.

Comment on lines +85 to +91
// Ctrl/Cmd+Enter sends; a bare Enter has to stay a newline in a multi-line composer.
private async Task OnKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter" && (e.CtrlKey || e.MetaKey))
{
await SendAsync();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent the textarea default action for Ctrl/Cmd+Enter.

Line 88 detects the shortcut, but it does not cancel the browser default action. The textarea can insert a newline after SendAsync clears the draft. The following oninput event can then write that newline back to Vm.Body.

Handle this shortcut in a DOM listener. Call preventDefault() only when Key is Enter and Ctrl or Meta is pressed. Keep bare Enter unchanged. Add a browser-level test that verifies the editor remains empty after sending.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClient.UI/Components/ComposeView.razor` around lines 85 - 91, Update
the ComposeView key handling around OnKeyDown so Ctrl/Cmd+Enter is processed by
a DOM listener that calls preventDefault() only for that shortcut, preventing
the textarea from inserting a newline while preserving bare Enter behavior.
Retain SendAsync() for the shortcut and add a browser-level test verifying the
editor remains empty after sending.

Comment on lines +1 to +5
@page "/diagnostics"

<PageTitle>SharpClient &middot; Diagnostics</PageTitle>

<DiagnosticsView InitialFilter="@Filter" />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hide the diagnostics view when log reading is unavailable.

Line 5 renders DiagnosticsView for every host. A direct navigation to /diagnostics still shows the viewer on the Web host, although the PR objective requires that host not to show it. Inject ILogReader and render the view only when Reader.IsAvailable is true.

Proposed fix
 `@page` "/diagnostics"
+@inject ILogReader Reader
 
 <PageTitle>SharpClient &middot; Diagnostics</PageTitle>
 
-<DiagnosticsView InitialFilter="`@Filter`" />
+@if (Reader.IsAvailable)
+{
+    <DiagnosticsView InitialFilter="`@Filter`" />
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@page "/diagnostics"
<PageTitle>SharpClient &middot; Diagnostics</PageTitle>
<DiagnosticsView InitialFilter="@Filter" />
`@page` "/diagnostics"
`@inject` ILogReader Reader
<PageTitle>SharpClient &middot; Diagnostics</PageTitle>
`@if` (Reader.IsAvailable)
{
<DiagnosticsView InitialFilter="`@Filter`" />
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SharpClient.UI/Pages/DiagnosticsPage.razor` around lines 1 - 5, Update
the DiagnosticsPage component to inject ILogReader as Reader and conditionally
render DiagnosticsView only when Reader.IsAvailable is true, while preserving
the existing InitialFilter binding.

Comment on lines +14 to +15
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500) =>
Task.FromResult<IReadOnlyList<LogEntry>>(Entries);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Respect maxEntries in the test reader.

Lines 14-15 return all entries regardless of maxEntries. This prevents tests from modeling a capped reader response and from validating truncation behavior. Return a capped snapshot.

Proposed fix
 public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500) =>
-    Task.FromResult<IReadOnlyList<LogEntry>>(Entries);
+    Task.FromResult<IReadOnlyList<LogEntry>>(Entries.Take(maxEntries).ToArray());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500) =>
Task.FromResult<IReadOnlyList<LogEntry>>(Entries);
public Task<IReadOnlyList<LogEntry>> ReadAsync(int maxEntries = 500) =>
Task.FromResult<IReadOnlyList<LogEntry>>(Entries.Take(maxEntries).ToArray());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/SharpClient.UI.Tests/UiFakeLogReader.cs` around lines 14 - 15, Update
UiFakeLogReader.ReadAsync to return a snapshot of Entries limited to maxEntries
rather than the entire collection. Preserve the IReadOnlyList<LogEntry> result
and ensure the original Entries collection is not modified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants